{-# OPTIONS_GHC -fno-warn-unused-binds #-}
{-# LANGUAGE OverloadedStrings, ForeignFunctionInterface, CPP, MagicHash,
             GeneralizedNewtypeDeriving #-}
-- | JSString standard functions, to make them a more viable alternative to
--   the horribly inefficient standard Strings.
--
--   Many functions have linear time complexity due to JavaScript engines not
--   implementing slicing, etc. in constant time.
--
--   All functions are supported on both client and server, with the exception
--   of 'match', 'matches', 'regex' and 'replace', which are wrappers on top of
--   JavaScript's native regular expressions and thus only supported on the
--   client.
module Haste.JSString
  ( JSString
  -- * Building JSStrings
  , empty, singleton, pack, cons, snoc, append, replicate
    -- * Deconstructing JSStrings
  , (!), unpack, head, last, tail, drop, take, init, splitAt
    -- * Examining JSStrings
  , null, length, any, all
    -- * Modifying JSStrings
  , map, reverse, intercalate, foldl', foldr, concat, concatMap
    -- * Regular expressions (client-side only)
  , RegEx, match, matches, regex, replace
    -- * JSString I/O
  , putStrLn, putStr
  ) where
import qualified Data.List
import Prelude hiding (foldr, concat, concatMap, reverse, map, all, any,
                       length, null, splitAt, init, take, drop, tail, head,
                       last, replicate, putStrLn, putStr)
import qualified Prelude
import Data.String
import Haste.Prim
import Haste.Prim.Foreign
import Control.Monad.IO.Class
import System.IO.Unsafe

#ifdef __HASTE__
import GHC.Prim

{-# INLINE d2c #-}
d2c :: Double -> Char
d2c d = unsafeCoerce# d

_jss_singleton :: Char -> IO JSString
_jss_singleton = ffi "String.fromCharCode"

_jss_cons :: Char -> JSString -> IO JSString
_jss_cons = ffi "(function(c,s){return String.fromCharCode(c)+s;})"

_jss_snoc :: JSString -> Char -> IO JSString
_jss_snoc = ffi "(function(s,c){return s+String.fromCharCode(c);})"

_jss_append :: JSString -> JSString -> IO JSString
_jss_append = ffi "(function(a,b){return a+b;})"

_jss_len :: JSString -> IO Int
_jss_len = ffi "(function(s){return s.length;})"

_jss_index :: JSString -> Int -> IO Double
_jss_index = ffi "(function(s,i){return s.charCodeAt(i);})"

_jss_substr :: JSString -> Int -> IO JSString
_jss_substr = ffi "(function(s,x){return s.substr(x);})"

_jss_take :: Int -> JSString -> IO JSString
_jss_take = ffi "(function(n,s){return s.substr(0,n);})"

_jss_rev :: JSString -> IO JSString
_jss_rev = ffi "(function(s){return s.split('').reverse().join('');})"

_jss_re_match :: JSString -> RegEx -> IO Bool
_jss_re_match = ffi "(function(s,re){return s.search(re)>=0;})"

_jss_re_compile :: JSString -> JSString -> IO RegEx
_jss_re_compile = ffi "(function(re,fs){return new RegExp(re,fs);})"

_jss_re_replace :: JSString -> RegEx -> JSString -> IO JSString
_jss_re_replace = ffi "(function(s,re,rep){return s.replace(re,rep);})"

_jss_re_find :: RegEx -> JSString -> IO [JSString]
_jss_re_find = ffi "(function(re,s) {\
var a = s.match(re);\
return a ? a : [];})"

{-# INLINE _jss_map #-}
_jss_map :: (Char -> Char) -> JSString -> JSString
_jss_map f s = unsafePerformIO $ cmap_js (_jss_singleton . f) s

{-# INLINE _jss_cmap #-}
_jss_cmap :: (Char -> JSString) -> JSString -> JSString
_jss_cmap f s = unsafePerformIO $ cmap_js (return . f) s

cmap_js :: (Char -> IO JSString) -> JSString -> IO JSString
cmap_js = ffi "(function(f,s){\
var s2 = '';\
for(var i in s) {\
   s2 += f(s.charCodeAt(i));\
}\
return s2;})"

{-# INLINE _jss_foldl #-}
_jss_foldl :: (ToAny a, FromAny a) => (a -> Char -> a) -> a -> JSString -> a
_jss_foldl f x s = fromOpaque . unsafePerformIO $ do
  foldl_js (\a c ->  toOpaque $ f (fromOpaque a) c) (toOpaque x) s

foldl_js :: (Opaque a -> Char -> Opaque a)
         -> Opaque a
         -> JSString
         -> IO (Opaque a)
foldl_js = ffi "(function(f,x,s){\
for(var i in s) {\
  x = f(x,s.charCodeAt(i));\
}\
return x;})"

{-# INLINE _jss_foldr #-}
_jss_foldr :: (ToAny a, FromAny a) => (Char -> a -> a) -> a -> JSString -> a
_jss_foldr f x s = fromOpaque . unsafePerformIO $ do
  foldr_js (\c -> toOpaque . f c . fromOpaque) (toOpaque x) s

foldr_js :: (Char -> Opaque a -> Opaque a)
         -> Opaque a
         -> JSString
         -> IO (Opaque a)
foldr_js = ffi "(function(f,x,s){\
for(var i = s.length-1; i >= 0; --i) {\
  x = f(s.charCodeAt(i),x);\
}\
return x;})"

#else

{-# INLINE d2c #-}
d2c :: Char -> Char
d2c = id

_jss_singleton :: Char -> IO JSString
_jss_singleton c = return $ toJSStr [c]

_jss_cons :: Char -> JSString -> IO JSString
_jss_cons c s = return $ toJSStr (c : fromJSStr s)

_jss_snoc :: JSString -> Char -> IO JSString
_jss_snoc s c = return $ toJSStr (fromJSStr s ++ [c])

_jss_append :: JSString -> JSString -> IO JSString
_jss_append a b = return $ catJSStr "" [a, b]

_jss_len :: JSString -> IO Int
_jss_len s = return $ Data.List.length $ fromJSStr s

_jss_index :: JSString -> Int -> IO Char
_jss_index s n = return $ fromJSStr s !! n

_jss_substr :: JSString -> Int -> IO JSString
_jss_substr s n = return $ toJSStr $ Data.List.drop n $ fromJSStr s

_jss_take :: Int -> JSString -> IO JSString
_jss_take n = return . toJSStr . Data.List.take n . fromJSStr

_jss_map :: (Char -> Char) -> JSString -> JSString
_jss_map f = toJSStr . Data.List.map f . fromJSStr

_jss_cmap :: (Char -> JSString) -> JSString -> JSString
_jss_cmap f =
  toJSStr . Data.List.concat . Data.List.map (fromJSStr . f) . fromJSStr

_jss_rev :: JSString -> IO JSString
_jss_rev = return . toJSStr . Data.List.reverse . fromJSStr

_jss_foldl :: (a -> Char -> a) -> a -> JSString -> a
_jss_foldl f x = Data.List.foldl' f x . fromJSStr

_jss_foldr :: (Char -> a -> a) -> a -> JSString -> a
_jss_foldr f x = Data.List.foldr f x . fromJSStr

_jss_re_compile :: JSString -> JSString -> IO RegEx
_jss_re_compile _ _ =
  error "Regular expressions are only supported client-side!"

_jss_re_match :: JSString -> RegEx -> IO Bool
_jss_re_match _ _ =
  error "Regular expressions are only supported client-side!"

_jss_re_replace :: JSString -> RegEx -> JSString -> IO JSString
_jss_re_replace _ _ _ =
  error "Regular expressions are only supported client-side!"

_jss_re_find :: RegEx -> JSString -> IO [JSString]
_jss_re_find _ _ =
  error "Regular expressions are only supported client-side!"

#endif

-- | A regular expression. May be used to match and replace JSStrings.
newtype RegEx = RegEx JSAny
  deriving (ToAny, FromAny)

instance IsString RegEx where
  fromString s = unsafePerformIO $ _jss_re_compile (fromString s) ""

-- | O(1) The empty JSString.
empty :: JSString
empty = ""

-- | O(1) JSString consisting of a single character.
singleton :: Char -> JSString
singleton = veryUnsafePerformIO . _jss_singleton

-- | O(n) Convert a list of Char into a JSString.
pack :: [Char] -> JSString
pack = toJSStr

-- | O(n) Convert a JSString to a list of Char.
unpack :: JSString -> [Char]
unpack = fromJSStr

infixr 5 `cons`
-- | O(n) Prepend a character to a JSString.
cons :: Char -> JSString -> JSString
cons c s = veryUnsafePerformIO $ _jss_cons c s

infixl 5 `snoc`
-- | O(n) Append a character to a JSString.
snoc :: JSString -> Char -> JSString
snoc s c = veryUnsafePerformIO $ _jss_snoc s c

-- | O(n) Append two JSStrings.
append :: JSString -> JSString -> JSString
append a b = veryUnsafePerformIO $ _jss_append a b

-- | O(1) Extract the first element of a non-empty JSString.
head :: JSString -> Char
head s =
#ifdef __HASTE__
  case veryUnsafePerformIO $ _jss_index s 0 of
    c | isNaN c   -> error "Haste.JSString.head: empty JSString"
      | otherwise -> d2c c -- Double/Int/Char share representation.
#else
  Data.List.head $ fromJSStr s
#endif

-- | O(1) Extract the last element of a non-empty JSString.
last :: JSString -> Char
last s =
  case veryUnsafePerformIO $ _jss_len s of
    0 -> error "Haste.JSString.head: empty JSString"
    n -> d2c (veryUnsafePerformIO $ _jss_index s (n-1))

-- | Get a single character from a JSString.
(!) :: JSString -> Int -> Char
s ! n =
#ifdef __HASTE__
  case veryUnsafePerformIO $ _jss_index s n of
    c | isNaN c   -> error "Haste.JSString.(!): index out of bounds"
      | otherwise -> d2c c -- Double/Int/Char share representation.
#else
  fromJSStr s !! n
#endif


-- | O(n) All elements but the first of a JSString. Returns an empty JSString
--   if the given JSString is empty.
tail :: JSString -> JSString
tail s = veryUnsafePerformIO $ _jss_substr s 1

-- | O(n) Drop 'n' elements from the given JSString.
drop :: Int -> JSString -> JSString
drop n s = veryUnsafePerformIO $ _jss_substr s (max 0 n)

-- | O(n) Take 'n' elements from the given JSString.
take :: Int -> JSString -> JSString
take n s = veryUnsafePerformIO $ _jss_take n s

-- | O(n) All elements but the last of a JSString. Returns an empty JSString
--   if the given JSString is empty.
init :: JSString -> JSString
init s = veryUnsafePerformIO $ _jss_take (veryUnsafePerformIO (_jss_len s)-1) s

-- | O(1) Test whether a JSString is empty.
null :: JSString -> Bool
null s = veryUnsafePerformIO (_jss_len s) == 0

-- | O(1) Get the length of a JSString as an Int.
length :: JSString -> Int
length = veryUnsafePerformIO . _jss_len

-- | O(n) Map a function over the given JSString.
map :: (Char -> Char) -> JSString -> JSString
map f s = _jss_map f s

-- | O(n) reverse a JSString.
reverse :: JSString -> JSString
reverse = veryUnsafePerformIO . _jss_rev

-- | O(n) Join a list of JSStrings, with a specified separator. Equivalent to
--   'String.join'.
intercalate :: JSString -> [JSString] -> JSString
intercalate = catJSStr

-- | O(n) Left fold over a JSString.
foldl' :: (ToAny a, FromAny a) => (a -> Char -> a) -> a -> JSString -> a
foldl' = _jss_foldl

-- | O(n) Right fold over a JSString.
foldr :: (ToAny a, FromAny a) => (Char -> a -> a) -> a -> JSString -> a
foldr = _jss_foldr

-- | O(n) Concatenate a list of JSStrings.
concat :: [JSString] -> JSString
concat = catJSStr ""

-- | O(n) Map a function over a JSString, then concatenate the results.
--   Note that this function is actually faster than 'map' in most cases.
concatMap :: (Char -> JSString) -> JSString -> JSString
concatMap = _jss_cmap

-- | O(n) Determines whether any character in the string satisfies the given
--   predicate.
any :: (Char -> Bool) -> JSString -> Bool
any p = Haste.JSString.foldl' (\a x -> a || p x) False

-- | O(n) Determines whether all characters in the string satisfy the given
--   predicate.
all :: (Char -> Bool) -> JSString -> Bool
all p = Haste.JSString.foldl' (\a x -> a && p x) False

-- | O(n) Create a JSString containing 'n' instances of a single character.
replicate :: Int -> Char -> JSString
replicate n c = Haste.JSString.pack $ Data.List.replicate n c

-- | O(n) Equivalent to (take n xs, drop n xs).
splitAt :: Int -> JSString -> (JSString, JSString)
splitAt n s = (Haste.JSString.take n s, Haste.JSString.drop n s)

-- | As 'Prelude.putStrLn'.
putStrLn :: MonadIO m => JSString -> m ()
putStrLn = liftIO . Prelude.putStrLn . unpack

-- | As 'Prelude.putStr'.
putStr :: MonadIO m => JSString -> m ()
putStr = liftIO . Prelude.putStr . unpack

-- | O(n) Determines whether the given JSString matches the given regular
--   expression or not.
matches :: JSString -> RegEx -> Bool
matches s re = veryUnsafePerformIO $ _jss_re_match s re

-- | O(n) Find all strings corresponding to the given regular expression.
match :: RegEx -> JSString -> [JSString]
match re s = veryUnsafePerformIO $ _jss_re_find re s

-- | O(n) Compile a regular expression and an (optionally empty) list of flags
--   into a 'RegEx' which can be used to match, replace, etc. on JSStrings.
--
--   The regular expression and flags are passed verbatim to the browser's
--   RegEx constructor, meaning that the syntax is the same as when using
--   regular expressions in raw JavaScript.
regex :: JSString -- ^ Regular expression.
      -> JSString -- ^ Potential flags.
      -> RegEx
regex re flags = veryUnsafePerformIO $ _jss_re_compile re flags

-- | O(n) String substitution using regular expressions.
replace :: JSString -- ^ String perform substitution on.
        -> RegEx    -- ^ Regular expression to match.
        -> JSString -- ^ Replacement string.
        -> JSString
replace s re rep = veryUnsafePerformIO $ _jss_re_replace s re rep
